Next.js 博客系统教程

在本教程中,我们将使用 Next.js 构建一个简单的博客系统。这个博客系统将包括文章的创建、显示、编辑和删除功能。我们将使用 Next.js 的静态站点生成(SSG)和服务端渲染(SSR)功能来优化性能和 SEO。

1. 环境搭建

首先,确保你已经安装了 Node.js 和 npm。然后,使用以下命令创建一个新的 Next.js 项目:

npx create-next-app@latest nextjs-blog
cd nextjs-blog

2. 创建第一个 Next.js 应用

在项目根目录下运行以下命令启动开发服务器:

npm run dev

打开浏览器并访问 http://localhost:3000,你应该会看到一个默认的 Next.js 欢迎页面。

3. 页面和路由

Next.js 使用文件系统作为路由。在 pages 目录下创建一个新的文件 index.js,这是博客的首页:

// pages/index.js
import Link from 'next/link';

export default function Home() {
  return (
    <div>
      <h1>Welcome to My Blog</h1>
      <Link href="/posts/first-post">
        <a>Read my first post</a>
      </Link>
    </div>
  );
}

在 pages/posts 目录下创建一个新的文件 first-post.js:

// pages/posts/first-post.js
export default function FirstPost() {
  return (
    <div>
      <h1>First Post</h1>
      <p>This is the content of my first post.</p>
    </div>
  );
}

现在,访问 http://localhost:3000/posts/first-post,你应该会看到你的第一篇博客文章。

4. 静态文件处理

在 public 目录下放置静态文件,例如图片。在 public 目录下创建一个 images 文件夹,并放入一张图片 logo.png。然后在 index.js 中引用这张图片:

// pages/index.js
import Link from 'next/link';

export default function Home() {
  return (
    <div>
      <img src="/images/logo.png" alt="Logo" />
      <h1>Welcome to My Blog</h1>
      <Link href="/posts/first-post">
        <a>Read my first post</a>
      </Link>
    </div>
  );
}

5. 服务端渲染 (SSR)

使用 getServerSideProps 函数来获取服务器端数据并渲染页面。在 pages/posts/first-post.js 中添加以下代码:

// pages/posts/first-post.js
export default function FirstPost({ data }) {
  return (
    <div>
      <h1>First Post</h1>
      <p>{data.content}</p>
    </div>
  );
}

export async function getServerSideProps() {
  // 模拟从数据库或API获取数据
  const data = {
    content: 'This is the content of my first post.',
  };

  return {
    props: {
      data,
    },
  };
}

6. 静态站点生成 (SSG)

使用 getStaticProps 和 getStaticPaths 函数来生成静态页面。在 pages/posts/[id].js 中创建一个新的动态路由:

// pages/posts/[id].js
export default function Post({ post }) {
  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </div>
  );
}

export async function getStaticPaths() {
  // 模拟从数据库或API获取所有文章ID
  const paths = [
    { params: { id: '1' } },
    { params: { id: '2' } },
  ];

  return {
    paths,
    fallback: false,
  };
}

export async function getStaticProps({ params }) {
  // 模拟从数据库或API获取单篇文章数据
  const posts = {
    '1': { title: 'First Post', content: 'This is the content of my first post.' },
    '2': { title: 'Second Post', content: 'This is the content of my second post.' },
  };

  const post = posts[params.id];

  return {
    props: {
      post,
    },
  };
}

7. 数据获取方法

我们已经使用了 getServerSideProps 和 getStaticProps 来获取数据。接下来,我们将使用 getStaticPaths 来生成动态路由。

8. API 路由

在 pages/api 目录下创建一个新的文件 posts.js,用于处理博客文章的 API 请求:

// pages/api/posts.js
export default function handler(req, res) {
  const posts = [
    { id: '1', title: 'First Post', content: 'This is the content of my first post.' },
    { id: '2', title: 'Second Post', content: 'This is the content of my second post.' },
  ];

  res.status(200).json(posts);
}

现在,访问 http://localhost:3000/api/posts,你应该会看到一个包含所有文章的 JSON 响应。

9. React 组件基础

在 components 目录下创建一个新的文件 Header.js,用于显示博客的头部:

// components/Header.js
import Link from 'next/link';

export default function Header() {
  return (
    <header>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/posts">
        <a>Posts</a>
      </Link>
    </header>
  );
}

在 pages/index.js 中使用这个组件:

// pages/index.js
import Link from 'next/link';
import Header from '../components/Header';

export default function Home() {
  return (
    <div>
      <Header />
      <h1>Welcome to My Blog</h1>
      <Link href="/posts/first-post">
        <a>Read my first post</a>
      </Link>
    </div>
  );
}

10. CSS Modules

在 components 目录下创建一个新的文件 Header.module.css,用于定义样式:

/* components/Header.module.css */
.header {
  display: flex;
  justify-content: space-between;
  padding: 1rem;
  background-color: #333;
  color: white;
}

.header a {
  color: white;
  text-decoration: none;
}

在 Header.js 中使用这个样式:

// components/Header.js
import Link from 'next/link';
import styles from './Header.module.css';

export default function Header() {
  return (
    <header className={styles.header}>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/posts">
        <a>Posts</a>
      </Link>
    </header>
  );
}

11. Sass 支持

Next.js 默认支持 Sass。在 components 目录下创建一个新的文件 Header.module.scss,用于定义样式:

/* components/Header.module.scss */
.header {
  display: flex;
  justify-content: space-between;
  padding: 1rem;
  background-color: #333;
  color: white;

  a {
    color: white;
    text-decoration: none;
  }
}

在 Header.js 中使用这个样式:

// components/Header.js
import Link from 'next/link';
import styles from './Header.module.scss';

export default function Header() {
  return (
    <header className={styles.header}>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/posts">
        <a>Posts</a>
      </Link>
    </header>
  );
}

12. Styled-components 集成

首先,安装 styled-components:

npm install styled-components

在 components 目录下创建一个新的文件 Header.js,使用 styled-components:

// components/Header.js
import Link from 'next/link';
import styled from 'styled-components';

const HeaderContainer = styled.header`
  display: flex;
  justify-content: space-between;
  padding: 1rem;
  background-color: #333;
  color: white;

  a {
    color: white;
    text-decoration: none;
  }
`;

export default function Header() {
  return (
    <HeaderContainer>
      <Link href="/">
        <a>Home</a>
      </Link>
      <Link href="/posts">
        <a>Posts</a>
      </Link>
    </HeaderContainer>
  );
}

13. 动态导入

使用 next/dynamic 来动态导入组件。在 pages/index.js 中动态导入 Header 组件:

// pages/index.js
import Link from 'next/link';
import dynamic from 'next/dynamic';

const Header = dynamic(() => import('../components/Header'));

export default function Home() {
  return (
    <div>
      <Header />
      <h1>Welcome to My Blog</h1>
      <Link href="/posts/first-post">
        <a>Read my first post</a>
      </Link>
    </div>
  );
}

14. 自定义 App 和 Document

在 pages/_app.js 中自定义 App 组件:

// pages/_app.js
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />;
}

export default MyApp;

在 pages/_document.js 中自定义 Document 组件:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

15. 中间件

在 pages/_middleware.js 中创建中间件:

// pages/_middleware.js
import { NextResponse } from 'next/server';

export function middleware(req) {
  const url = req.nextUrl.clone();
  if (url.pathname === '/') {
    url.pathname = '/posts';
    return NextResponse.redirect(url);
  }
}

16. 国际化

使用 next-i18next 来实现国际化。首先,安装 next-i18next:

npm install next-i18next

在 next.config.js 中配置国际化:

// next.config.js
const { i18n } = require('./next-i18next.config');

module.exports = {
  i18n,
};

在 next-i18next.config.js 中配置语言:

// next-i18next.config.js
module.exports = {
  i18n: {
    defaultLocale: 'en',
    locales: ['en', 'es'],
  },
};

在 pages/index.js 中使用国际化:

// pages/index.js
import { useTranslation } from 'next-i18next';
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export default function Home() {
  const { t } = useTranslation('common');

  return (
    <div>
      <h1>{t('welcome')}</h1>
    </div>
  );
}

export async function getStaticProps({ locale }) {
  return {
    props: {
      ...(await serverSideTranslations(locale, ['common'])),
    },
  };
}

17. 图像优化

使用 next/image 组件来优化图像:

// pages/index.js
import Image from 'next/image';

export default function Home() {
  return (
    <div>
      <h1>Welcome to My Blog</h1>
      <Image src="/images/logo.png" alt="Logo" width={200} height={200} />
    </div>
  );
}

18. 字体优化

在 pages/_document.js 中添加自定义字体:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head>
          <link
            href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
            rel="stylesheet"
          />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;

19. 部署到 Vercel

使用 Vercel 部署你的 Next.js 应用。首先,安装 Vercel CLI:

npm install -g vercel

然后,在项目根目录下运行以下命令:

vercel

按照提示完成部署。

20. 自定义服务器

在 server.js 中创建自定义服务器:

// server.js
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

app.prepare().then(() => {
  createServer((req, res) => {
    const parsedUrl = parse(req.url, true);
    handle(req, res, parsedUrl);
  }).listen(3000, (err) => {
    if (err) throw err;
    console.log('> Ready on http://localhost:3000');
  });
});

在 package.json 中添加启动脚本:

{
  "scripts": {
    "dev": "node server.js",
    "build": "next build",
    "start": "NODE_ENV=production node server.js"
  }
}

21. 性能优化技巧

使用 next/image 组件来优化图像,使用 next/link 组件来预加载页面,使用 next/script 组件来优化脚本加载。

22. SEO 最佳实践

在 pages/_document.js 中添加元数据:

// pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return { ...initialProps };
  }

  render() {
    return (
      <Html>
        <Head>
          <meta name="description" content="My Blog" />
          <meta name="keywords" content="blog, nextjs"